Go后台项目架构思考与重构 | 深度长文
The following article is from CSDN Author 黄雷
架构的重要性;
重构的几种模式;
设计原则;
DDD 中领域思想;
项目的可测试性;
项目的可演进性。
一个好的架构,其终极目标应当是,用最小的人力成本满足构建和维护该系统的需求。
可运行不可变软件,最终会因为无法改变而导致行为无法迭代或者迭代慢而变成没有价值。可变不可运行的软件,可通过迭代,变成可运行可变软件,所以架构比行为重要。
虽然我们也反对过度设计,但是识别,或者说猜测项目未来符合逻辑的可能变动,将架构设计考虑进项目早期是十分有必要的,架构设计和调整应该贯穿项目的整个成长过程。
人力消耗巨大,需要一边加新需求一边重写旧需求;
无法确保新的工程的设计比旧的好;
重写过程中可能出现业务遗漏。
不希望存在多个服务共存的问题;
希望共享旧工程的 CICD,运维,监控等能力;
重构颗粒度过大,我们希望细到函数级别的重构。
MVC Controller:用于接收 HTTP 请求,并调用 Service 进行业务处理;
MVC Service:核心业务逻辑全部落在这一层;
MVC DAO:DB 相关操作都在这一层;
MVC Models: 包含各个对象的字段,比如集群、节点等;
Controller 模式下的各个 Controller:每个 Controller 逻辑差异很大,但是都是调用 Service 进行对象状态的初始化或者设置;
Components:调用外部服务的模块都在这里,比如调用计算资源服务创建虚拟机、调用网络资源服务设置网络等。
每一层一个包
依赖关系混乱
各层之间权责不明
每层内部没有设计
贫血模型导致 DAO 层臃肿
无法单测
模块划分不清
Controller 模式能力不足
外部依赖接口化
充血模型
DB 依赖接口化
异步流程
Service 分包
可测试
考虑节点升级请求参数比较复杂,没法存在现有表中,需要新建一个表用于存储节点升级的参数和进度。
编写对应的 Models。
编写专门用于上述表的 DAO 层代码。
编写一个 Controller 异步流程,要为该 Controller 专门实现暂停,取消等控制机制。
编写专门的旁路进行监控告警。
Service 中实现节点升级核心流程。
由于无法单测,觉得写得差不多了,需要等待测试环境空闲时,部署到测试环境进行调试。注意,测试环境是公共的,别人可能也需要用。
由于 Task 框架已经提供了参数,进度的存储,以及 Task 相关的 DAO 代码,所以不需要创建任何新的 DB 表;
由于 Task 已经实现了统一的暂停,取消等任务控制机制,不需要编写相关代码;
创建一个 Task Handler,实现节点升级;
Task 有统一的监控,无需重复编写;
由于 Skipper 是可单测的,在部署到测试环境之前,我们通过单元测试快速调通了核心逻辑;
部署到测试环境进行集成测试,这时候 Bug 已经很少了。
core obj 过度设计
全局依赖
模块不内聚
架构设计原则
SRP:单一职责原则
任何一个软件模块都应该有且仅有一个原因被修改。
任何一个软件模块都应该只对一类行为者负责。
OCP:开闭原则
设计良好的计算机软件应该易于扩展,同时抗拒修改。
LSP:里氏替换原则
这里需要一种可替换性:如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
所有引用基类的地方必须能透明地使用其子类的对象。
假设存在接口 A 的实现 Aa 和 Ab,使用接口 A 的程序在传入的具体实现由 Aa 改成 Ab 时,行为不发生变化。
ISP:接口隔离原则
客户端不应该依赖它不需要的接口。
DIP:依赖反转原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
CCP:共同闭包原则
应该将那些会同时修改,并且为相同目的而修改的类放在同一个组件中,而将不会同时修改,并且不会为了相同目的的修改的那些类放在不同组件中。
将由于相同原因而需改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西放在一起。
CRP:共同复用原则
不要强迫一个组件的用户依赖他们不需要的东西。
ADP:无依赖环原则
组件依赖关系图中不应该出现环。
SDP:稳定依赖原则
依赖关系必须要指向更稳定的方向。
SAP:稳定抽象原则
一个组件的抽象化程度应该与其稳定性保持一致。
依赖关系应该指向更加抽象的方向。
水平分层
领域划分与边界
领域事件
我们将 v1 中的 service 层切成两层,把跨多领域的业务逻辑上拉至 application 层中,让剩下的业务逻辑包含明显的业务边界;
我们再根据各个业务模块的依赖关系紧密程度进行重组,形成领域,每个领域只处理自己领域的业务,每个领域对外暴露一套 Service 接口用于描述该领域对外暴露的能力,领域可以利用 Event Bus 对外发布事件,用于通知外部领域内正在发生的事;
原来全局公用的存储层,现在分散到各个领域自行维护,不同领域可以采用不同的存储;
原来放置全局的 Controller 和 Task Handler,现在由每个领域自行管理,系统依然提供 Controller 和 Task 的引擎(由 Task 领域负责)。这使得领域业务逻辑更加内聚;
注意各模块的依赖关系,我们尽量遵循稳定依赖原则和稳定抽象原则,不稳定模块尽量依赖于稳定模块,如果需要让稳定模块依赖于不稳定模块,我们引入 Interface 进行抽象。
推荐阅读
如何通过深度学习,完成计算机视觉中的所有工作? 看似毫不相干,哲学与机器学习竟有如此大的交集
黑客用上机器学习你慌不慌?这 7 种窃取数据的新手段快来认识一下 “谷歌杀手”发明者,科学天才 Wolfram 清晰架构的 Go 微服务: 程序容器 5分钟!就能学会以太坊 JSON API 基础知识
你点的每个“在看”,我都认真当成了AI